/*
 * @(#) $Header$
 *
 * Copyright (C) 2008  Forklabs Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.media.jai.operator;

import java.awt.RenderingHints;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import javax.media.jai.JAI;
import javax.media.jai.OperationDescriptor;
import javax.media.jai.OperationDescriptorImpl;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.ROI;
import javax.media.jai.RenderedOp;
import javax.media.jai.registry.RenderedRegistryMode;
import javax.media.jai.util.Range;

/**
 * Class {@code PrecisionAndRecallDescriptor} is an {@link OperationDescriptor}
 * describing the <em>precision</em>, <em>recall</em> and
 * <em>precision+recall</em> operation. This operation calculates the
 * <a href="http://en.wikipedia.org/wiki/Precision_and_Recall">precision and recall</a>
 * of an image. All three, <em>precision</em>, <em>recall</em> and
 * <em>precision+recall</em> are synonyms.
 * <p>
 * About precision: it is the total number of pixels that are both in the
 * candidate class and the truth class whose total is divided by the number of
 * pixels in the candidate class.
 * <p>
 * About recall: it is the total number of pixels that are both in the candidate
 * class and the truth class whose total is divided by the number of pixels in
 * the truth class.
 * </p>
 * This operation takes three main parameters: - the color of the pixels in the
 * candidate image for the class to calculate the precision for, - the truth
 * image and - the color of the pixels in the truth image for the class to
 * calculate the precision for.
 * <p>
 * Both color parameters are in the form of {@code int[]}. The length of
 * these arrays must match the number of bands in the corresponding image.
 * <p>
 * The operator also takes three minor parameters: - the region of interest,
 * - the horizontal sampling rate and - the vertical sampling rate.
 * <p>
 * Finally, the images need not have the same number of bands, but they do need
 * to be of the same dimensions.
 *
 * <table border=1>
 * <caption>Resource List</caption>
 * <tr><th>Name</th>        <th>Value</th></tr>
 * <tr><td>GlobalName</td>  <td>precisionandrecall</td></tr>
 * <tr><td>LocalName</td>   <td>precisionandrecall</td></tr>
 * <tr><td>Vendor</td>      <td>ca.forklabs.media.jai.opimage</td></tr>
 * <tr><td>Description</td> <td>Precision and Recall statistical value</td></tr>
 * <tr><td>DocURL</td>      <td>n/a</td></tr>
 * <tr><td>Version</td>     <td>$Version$</td></tr>
 * <tr><td>Arg0Desct</td>   <td>The color of the class in the candidate image</td></tr>
 * <tr><td>Arg1Desct</td>   <td>The truth image</td></tr>
 * <tr><td>Arg2Desct</td>   <td>The color of the class in the truth image</td></tr>
 * <tr><td>Arg3Desct</td>   <td>The region of interest</td></tr>
 * <tr><td>Arg4Desct</td>   <td>The horizontal sampling rate</td></tr>
 * <tr><td>Arg5Desct</td>   <td>The vertical sampling rate</td></tr>
 * </table>
 *
 * <table border=1>
 * <caption>Parameter List</caption>
 * <tr><th>Name</th>            <th>Class Type</th>            <th>Default Value</th></tr>
 * <tr><td>candidatecolor</td>  <td>int[]</td>                 <td>NO_PARAMETER_DEFAULT</td>
 * <tr><td>truthimage</td>      <td>RenderedImage</td>         <td>NO_PARAMETER_DEFAULT</td>
 * <tr><td>truthcolor</td>      <td>int[]</td>                 <td>NO_PARAMETER_DEFAULT</td>
 * <tr><td>roi</td>             <td>javax.media.jai.ROI</td>   <td>null</td>
 * <tr><td>xperiod</td>         <td>int</td>                   <td>1</td>
 * <tr><td>yperiod</td>         <td>int</td>                   <td>1</td>
 * </table>
 *
 * This operators works in {@code rendered} mode only.
 * <p>
 * The two statistics are accessed using {@code getProperty()} with the values
 * {@link #PRECISION_STATISTIC_NAME} and {@link #RECALL_STATISTIC_NAME}.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.media.jai.operator.PrecisionAndRecallDescriptor">Daniel Léonard</a>
 * @version $Revision$
 */
public class PrecisionAndRecallDescriptor extends OperationDescriptorImpl {

//---------------------------
// Class variables
//---------------------------

   /** <em>serialVersionUID</em>. */
   private static final long serialVersionUID = 5217545851566332293L;

   /** The name of this operator. */
   public static final String NAME = "PrecisionAndRecall"; //$NON-NLS-1$

   /** The name of the precision statistic this operator accumulates. */
   public static final String PRECISION_STATISTIC_NAME = "precision"; //$NON-NLS-1$
   /** The name of the recall statistic this operator accumulates. */
   public static final String RECALL_STATISTIC_NAME = "recall"; //$NON-NLS-1$

   /** The name of the candidate class parameter. */
   @SuppressWarnings("nls")
   public static final String CANDIDATE_COLOR_PARAMETER_NAME = "candidatecolor";
   /** The name of the truth image parameter. */
   @SuppressWarnings("nls")
   public static final String TRUTH_IMAGE_PARAMETER_NAME = "truthimage";
   /** The name of the truth class parameter. */
   @SuppressWarnings("nls")
   public static final String TRUTH_COLOR_PARAMETER_NAME = "truthcolor";
   /** The name of the region of interest parameter. */
   @SuppressWarnings("nls")
   public static final String REGION_OF_INTEREST_PARAMETER_NAME = "roi";
   /** The name of the horizontal sampling parameter. */
   @SuppressWarnings("nls")
   public static final String HORIZONTAL_SAMPLING_PARAMETER_NAME = "xperiod";
   /** The name of the vertical sampling parameter. */
   @SuppressWarnings("nls")
   public static final String VERTICAL_SAMPLING_PARAMETER_NAME = "yperiod";

   /** The index in the parameter block of the candidate class parameter. */
   public static final int CANDIDATE_COLOR_PARAMETER_INDEX = 0;
   /** The index in the parameter block of the truth image parameter. */
   public static final int TRUTH_IMAGE_PARAMETER_INDEX = 1;
   /** The index in the parameter block of the truth class parameter. */
   public static final int TRUTH_COLOR_PARAMETER_INDEX = 2;
   /** The index in the parameter block of the region of interest parameter. */
   public static final int REGION_OF_INTEREST_PARAMETER_INDEX = 3;
   /** The index in the parameter block of the horizontal sampling parameter. */
   public static final int HORIZONTAL_SAMPLING_PARAMETER_INDEX = 4;
   /** The index in the parameter block of the vertical sampling parameter. */
   public static final int VERTICAL_SAMPLING_PARAMETER_INDEX = 5;

   /**
    * The resource strings that provide the general documentation and specify
    * the parameter list for this operation.
    */
   private static final String[][] RESOURCES =
   {
      { "GlobalName",  PrecisionAndRecallDescriptor.NAME, }, //$NON-NLS-1$
      { "LocalName",   PrecisionAndRecallDescriptor.NAME, }, //$NON-NLS-1$
      { "Vendor",      "ca.forklabs.media.jai.opimage", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "Description", PrecisionAndRecallDescriptor.getDescription(), }, //$NON-NLS-1$
      { "DocURL",      "n/a", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "Version",     "$Version$", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "arg0Desc",    PrecisionAndRecallDescriptor.getArg0Description(), }, //$NON-NLS-1$
      { "arg1Desc",    PrecisionAndRecallDescriptor.getArg1Description(), }, //$NON-NLS-1$
      { "arg2Desc",    PrecisionAndRecallDescriptor.getArg2Description(), }, //$NON-NLS-1$
      { "arg3Desc",    PrecisionAndRecallDescriptor.getArg3Description(), }, //$NON-NLS-1$
      { "arg4Desc",    PrecisionAndRecallDescriptor.getArg4Description(), }, //$NON-NLS-1$
      { "arg5Desc",    PrecisionAndRecallDescriptor.getArg5Description(), }, //$NON-NLS-1$
   };

   /** The supported modes. */
   private static final String[] SUPPORTED_MODES =
   {
      RenderedRegistryMode.MODE_NAME,
   };

   /** The parameter class list for this operation. */
   private static final Class<?>[] PARAMETER_CLASSES = new Class<?>[]
   {
      int[].class,
      RenderedImage.class,
      int[].class,
      ROI.class,
      Integer.class,
      Integer.class,
   };

   /** The parameter name list for this operation. */
   private static final String[] PARAMETER_NAMES = new String[]
   {
      PrecisionAndRecallDescriptor.CANDIDATE_COLOR_PARAMETER_NAME,
      PrecisionAndRecallDescriptor.TRUTH_IMAGE_PARAMETER_NAME,
      PrecisionAndRecallDescriptor.TRUTH_COLOR_PARAMETER_NAME,
      PrecisionAndRecallDescriptor.REGION_OF_INTEREST_PARAMETER_NAME,
      PrecisionAndRecallDescriptor.HORIZONTAL_SAMPLING_PARAMETER_NAME,
      PrecisionAndRecallDescriptor.VERTICAL_SAMPLING_PARAMETER_NAME,
   };

   /** The default parameters. */
   @SuppressWarnings({ "static-access", "boxing" })
   private static final Object[] PARAMETER_DEFAULTS = new Object[]
   {
      PrecisionAndRecallDescriptor.NO_PARAMETER_DEFAULT,
      PrecisionAndRecallDescriptor.NO_PARAMETER_DEFAULT,
      PrecisionAndRecallDescriptor.NO_PARAMETER_DEFAULT,
      null,
      1,
      1,
   };

   /** Valid parameter values. */
   @SuppressWarnings("boxing")
   private static final Object[] VALID_PARAMETERS = new Object[]
   {
      null,
      null,
      null,
      null,
      new Range(Integer.class, 1, Integer.MAX_VALUE),
      new Range(Integer.class, 1, Integer.MAX_VALUE),
   };


//---------------------------
// Constructor
//---------------------------

   /**
    * Constructor.
    */
   public PrecisionAndRecallDescriptor() {
      super(RESOURCES, SUPPORTED_MODES, 1, PARAMETER_NAMES, PARAMETER_CLASSES, PARAMETER_DEFAULTS, VALID_PARAMETERS);
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Validates that an image and its class have the same number of bands.
    * @param   image   the image.
    * @param   color   the color of the class.
    * @param   sb   used for an error message if any.
    * @return   {@code true} if the number of bands matcher,
    *           {@code false} otherwise.
    */
   protected boolean validateBands(RenderedImage image, int[] color, StringBuffer sb) {
      boolean all_ok = true;

      SampleModel sample_model = image.getSampleModel();
      int image_bands = sample_model.getNumBands();

      int color_bands = color.length;

      all_ok = (image_bands == color_bands);
      if (false == all_ok) {
         String message = this.getBandsDoNotMatchErrorMessage(image_bands, color_bands);
         sb.append(message);
         return all_ok;
      }

      return all_ok;
      }


//---------------------------
// Overridden methods from javax.media.jai.OperationDescriptorImpl
//---------------------------

   /**
    * Validates that the color of the class matches the number of bands in the
    * image.
    * @param   mode   the rendering mode.
    * @param   pb   the parameter block.
    * @param   sb   the error message buffer.
    * @return   {@code true} if all is OK, {@code false} otherwise.
    */
   @Override
   public boolean validateParameters(String mode, ParameterBlock pb, StringBuffer sb) {
      boolean all_ok = super.validateParameters(mode, pb, sb);
      if (false == all_ok) {
         return all_ok;
         }

      RenderedImage image;
      int[] color;

      image = (RenderedImage) pb.getSource(0);
      color = (int[]) pb.getObjectParameter(PrecisionAndRecallDescriptor.CANDIDATE_COLOR_PARAMETER_INDEX);
      all_ok = this.validateBands(image, color, sb);
      if (false == all_ok) {
         return all_ok;
         }

      image = (RenderedImage) pb.getObjectParameter(PrecisionAndRecallDescriptor.TRUTH_IMAGE_PARAMETER_INDEX);
      color = (int[]) pb.getObjectParameter(PrecisionAndRecallDescriptor.TRUTH_COLOR_PARAMETER_INDEX);
      all_ok = this.validateBands(image, color, sb);
      if (false == all_ok) {
         return all_ok;
         }

      return all_ok;
      }


//---------------------------
// Class methods
//---------------------------

   /**
    * Creates and fills a new parameter block.
    * @param   mode   the rendering mode.
    * @param   source   the source image.
    * @param   candidate_color   the color of the pixels in the candidate image.
    * @param   truth   the truth image.
    * @param   truth_color   the color of the pixels in the truth image.
    * @param   roi   the region of interest.
    * @param   h_sample   the horizontal sampling rate.
    * @param   v_sample   the vertical sampling rate.
    * @return   a new parameter block.
    */
   public static ParameterBlockJAI createParameterBlock(String mode, Object source, int[] candidate_color, RenderedImage truth, int[] truth_color,
                                                        ROI roi, int h_sample, int v_sample) {
      String name = PrecisionAndRecallDescriptor.NAME;
      ParameterBlockJAI pb = new ParameterBlockJAI(name, mode);
      if (null != source) {
         pb.addSource(source);
         }
      pb.setParameter(PrecisionAndRecallDescriptor.CANDIDATE_COLOR_PARAMETER_NAME, candidate_color);
      pb.setParameter(PrecisionAndRecallDescriptor.TRUTH_IMAGE_PARAMETER_NAME, truth);
      pb.setParameter(PrecisionAndRecallDescriptor.TRUTH_COLOR_PARAMETER_NAME, truth_color);
      pb.setParameter(PrecisionAndRecallDescriptor.REGION_OF_INTEREST_PARAMETER_NAME, roi);
      pb.setParameter(PrecisionAndRecallDescriptor.HORIZONTAL_SAMPLING_PARAMETER_NAME, h_sample);
      pb.setParameter(PrecisionAndRecallDescriptor.VERTICAL_SAMPLING_PARAMETER_NAME, v_sample);
      return pb;
      }

   /**
    * Creates a rendered image.
    * @param   source   the source image.
    * @param   candidate_color   the color of the pixels in the candidate image.
    * @param   truth   the truth image.
    * @param   truth_color   the color of the pixels in the truth image.
    * @param   roi   the region of interest.
    * @param   h_sample   the horizontal sampling rate.
    * @param   v_sample   the vertical sampling rate.
    * @param   hints    the rendering hints, may be {@code null}.
    * @return   the new image.
    */
   public static RenderedOp create(RenderedImage source, int[] candidate_color, RenderedImage truth, int[] truth_color,
                                   ROI roi, int h_sample, int v_sample,
                                   RenderingHints hints) {
      String operation = PrecisionAndRecallDescriptor.NAME;
      String mode = RenderedRegistryMode.MODE_NAME;
      ParameterBlockJAI pb = PrecisionAndRecallDescriptor.createParameterBlock(mode, source, candidate_color, truth, truth_color, roi, h_sample, v_sample);
      RenderedOp image = JAI.create(operation, pb, hints);
      return image;
      }


//---------------------------
// External resources methods
//---------------------------

   /**
    * Gets the description of this operation.
    * @return   the description message.
    */
   protected static String getDescription() {
      String key = Resources.PRECISION_RECALL_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the first argument, the color of the class in
    * the candidate image.
    * @return   the description message.
    */
   protected static String getArg0Description() {
      String key = Resources.PRECISION_RECALL_ARG0_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the second argument, the truth image.
    * @return   the description message.
    */
   protected static String getArg1Description() {
      String key = Resources.PRECISION_RECALL_ARG1_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the third argument, the color of the class in
    * the truth image.
    * @return   the description message.
    */
   protected static String getArg2Description() {
      String key = Resources.PRECISION_RECALL_ARG2_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the fourth argument, the region of interest for
    * the statistics.
    * @return   the description message.
    */
   protected static String getArg3Description() {
      String key = Resources.PRECISION_RECALL_ARG3_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the fifth argument, the horizontal sampling.
    * @return   the description message.
    */
   protected static String getArg4Description() {
      String key = Resources.PRECISION_RECALL_ARG4_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the sixth argument, the vertical sampling.
    * @return   the description message.
    */
   protected static String getArg5Description() {
      String key = Resources.PRECISION_RECALL_ARG5_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the error message telling that the number of bands in the color
    * map do not match the number of bands in the source image.
    * @param   expected   the number of bands in the image.
    * @param   got   the number of bands in the color map.
    * @return   the error message.
    */
   @SuppressWarnings("boxing")
   protected String getBandsDoNotMatchErrorMessage(int expected, int got) {
      String key = Resources.PRECISION_RECALL_BAD_BANDS;
      String message = Resources.getLocalizedString(key, expected, got);
      return message;
      }

   }
